﻿Imports SilverMemory.Engine
Imports System.Threading

''' <summary>
''' computer player in the game
''' </summary>
''' <remarks></remarks>
Public Class ArtificialPlayer

  Public ReadOnly Player As Player

  Private Const THINKING_TIME As Integer = 1500 '1,5 sec "thinking time" before each turn
  Private game As Game

  'cards that this player has seen so far
  Private knownCards As New Dictionary(Of Integer, Card)

  'cards to turn in the next move
  Private goodMoves As New Stack(Of Card)

  Private Shared random As New Random(Now.Millisecond)
  Private lockedGamestate As GameState.AbstractGameState
  Private forgetOneOfX As Integer = 4
  Private mistakeOneOfX As Integer = 4

  ''' <summary>
  ''' constructor
  ''' </summary>
  ''' <param name="game">the game that this player plays in</param>
  ''' <param name="player">the game player this artificial player represents.</param>
  ''' <param name="level">0-10 integer: 0: player plays really bad, 10, player plays extremely good.</param>
  ''' <remarks></remarks>
  Public Sub New(ByVal game As Game, ByVal player As Player, ByVal level As Integer)
    Me.game = game
    Me.Player = player

    'trigger for when its my turn
    AddHandler Me.game.GameStateChanged, AddressOf evaluateMyTurnState

    For Each card As Card In Me.game.GameState.CardsList
      'add handlers for all card turns
      AddHandler card.Turned, AddressOf Card_Turned
    Next

    If level < 0 Then
      level = 0
    End If
    Me.forgetOneOfX = level
    Me.mistakeOneOfX = level
  End Sub

  ''' <summary>
  ''' evaluate changes in turnstate: make move if it's my turn.
  ''' </summary>
  Private Sub evaluateMyTurnState(ByVal sender As Object, ByVal e As EventArgs)
    'check if a move can be made
    If TypeOf Me.game.GameState Is GameState.FirstMoveGameState _
    OrElse TypeOf Me.game.GameState Is GameState.SecondMoveGameState Then

      'check if it is me who should make a move
      Select Case Player.TurnState
        Case Engine.Player.TurnStateE.MayMakeFirstMove, Engine.Player.TurnStateE.MayMakeSecondMove
          'lock the gamestate, it is  my turn
          Me.lockedGamestate = Me.game.LockGamestateExclusively()

          'do turn on different thread, so it looks
          'as if i'm thinking
          Dim lThread As New Thread(AddressOf doTurn)
          lThread.Start()
      End Select
    End If
  End Sub

  ''' <summary>
  ''' do theturn
  ''' </summary>
  ''' <remarks></remarks>
  Private Sub doTurn()
    'maximum tries to select a yet unknown card
    Dim maxTries As Integer = CInt(Math.Pow(Me.lockedGamestate.CardsList.Count, 2))

    'act thinking
    Thread.Sleep(ArtificialPlayer.THINKING_TIME)

    'find a card to turn from the goodmoves stack
    Dim cardToTurn As Card = findGoodMove()

    'act humanly
    cardToTurn = toForgetOrNotToForget(cardToTurn)
    Dim saveCardToTurn = cardToTurn
    cardToTurn = toTakeOrToMistake(cardToTurn)

    Dim tries = 0
    If Not canBeTaken(cardToTurn) Then
      'did not find a good move
      'pick a random card
      Do
        Dim randomNumber = getNextRandomNumber(Me.lockedGamestate.CardsList.Count)
        cardToTurn = Me.lockedGamestate.CardsList(randomNumber)
        tries += 1
        'try to find a yet unknown card, but it could be that there are no unknown cards left
        'so escape clause is number of tries, but a stupid player (iq < 4) also doesn't take this into account
      Loop Until canBeTaken(cardToTurn) And (Not knownCards.ContainsValue(cardToTurn) Or tries > maxTries Or mistakeOneOfX < 4)
    End If

    'do it, unlocking the gamestate
    Me.game.CardSelected(cardToTurn, lockedGamestate)
  End Sub

  ''' <summary>
  ''' find a card to turn from our good moves stack
  ''' </summary>
  Private Function findGoodMove() As Card
    'pop cards from the stack that are not taken or turned
    Dim card As Card = Nothing
    While Not canBeTaken(card) And goodMoves.Count > 0
      card = goodMoves.Pop()
    End While

    If canBeTaken(card) Then
      Return card
    End If

    'no card found
    Return Nothing
  End Function

  ''' <summary>
  ''' determines wether this a card that can safely be taken or not
  ''' </summary>
  Private Function canBeTaken(ByVal card As Card) As Boolean
    Return card IsNot Nothing AndAlso Not card.Taken AndAlso card.Facing = Engine.Card.Face.Down
  End Function

  ''' <summary>
  ''' return next random number in a threadsafe way
  ''' </summary>
  Private Shared Function getNextRandomNumber(ByVal max As Integer) As Integer
    SyncLock ArtificialPlayer.random
      Return ArtificialPlayer.random.Next(max)
    End SyncLock
  End Function

  ''' <summary>
  ''' forget the card sometimes to simulate a real player and to 
  ''' make the game more fun to mortals
  ''' </summary>
  Private Function toForgetOrNotToForget(ByVal card As Card) As Card
    'if the random number = 0, forget the card. The ratio makes
    'it more probable the random number = 0, because it narrows the range (from 0 to ...)
    'a forget ratio of 1 will always result in a forget.
    Dim number = getNextRandomNumber(forgetOneOfX)
    Dim forget = (number = 0)

    If forget Then
      Logger.Log("Forget!")
      Return Nothing
    End If
    Return card
  End Function

  ''' <summary>
  ''' take the wrong card sometimes to act more like a mortal player
  ''' </summary>
  Private Function toTakeOrToMistake(ByVal card As Card) As Card
    'if random number = 0, make mistake
    Dim number = getNextRandomNumber(mistakeOneOfX)
    Dim mistake = (number = 0)

    If mistake Then
      Logger.Log("Mistake!")
      With Me.lockedGamestate.CardsList
        For i As Integer = 0 To .Count - 1
          If .Item(i) Is card Then
            'take the next or previous card
            Dim whatToDo = getNextRandomNumber(2)
            If whatToDo = 1 AndAlso i > 0 AndAlso canBeTaken(.Item(i - 1)) Then
              'take previous
              Return .Item(i - 1)
            ElseIf whatToDo = 0 AndAlso i < .Count - 1 AndAlso canBeTaken(.Item(i + 1)) Then
              'take next
              Return .Item(i + 1)
            Else
              'mistake would be too stupid by now
              Return card
            End If
          End If
        Next
      End With
    End If

    Return card
  End Function

  ''' <summary>
  ''' remember turned cards
  ''' </summary>
  Private Sub Card_Turned(ByVal sender As Object, ByVal e As CardTurnedEventArgs)
    Dim card As Card = CType(sender, Card)

    'compare with previous cards
    For Each knownCard In knownCards.Values
      If knownCard.TwinCard Is card Then
        'bwuhahaha die die you set of cards
        If Not goodMoves.Contains(card) Then
          goodMoves.Push(card)
          goodMoves.Push(knownCard)
        End If
      End If
    Next

    'remember the card
    If Not knownCards.ContainsValue(card) Then
      knownCards.Add(knownCards.Count, card)
    End If
  End Sub

End Class
